package MT::App::Test;

use warnings;
use strict;
use Carp;

use version; our $VERSION = qv('0.1.0');

use File::Basename;
use File::Spec;
use File::Path;
use File::Copy;

use YAML::Tiny;
use JSON;

use IPC::Open2;
use IO::Select;

use MT::App::Test::Object;


# default mt-config.cgi
my $default_config = File::Spec->catfile(dirname(__FILE__), 'mt-config.cgi');

# default parameters
my %default_param = (
	base_dir => '',

	fixtures => [
		File::Spec->catdir(dirname(__FILE__), 'fixtures')
	],
	volatiles => [
		File::Spec->catdir(dirname(__FILE__), 'volatiles')
	],
	plugindatas => [
		File::Spec->catdir(dirname(__FILE__), 'plugindatas')
	],
	templatedatas => [
		File::Spec->catdir(dirname(__FILE__), 'templatedatas')
	],
	requests => [
		File::Spec->catdir(dirname(__FILE__), 'requests')
	],

	force_initialize_database => 0,
	print_upgrade_progress => 0,

	# parameters for sqlite
	sqlite_database => 'mt-sqlite.db',
	sqlite_dump_file => 'mt-sqlite-dump.db',
	
	# parameters for mysql
	mysql_dump_file => 'mt-mysql-dump.db',
	mysql => 'mysql',
	mysqldump => 'mysqldump',
	
	# parameters for postgres
	postgres_dump_file => 'mt-postgres-dump.db',
	psql => 'psql',
	pg_dump => 'pg_dump',
	
	# parameters for php
	php => 'php',
);

# cache
my %initial_cache_status = (
	'test_fixtures' => undef,
	'test_volatiles' => undef,
	'test_plugindatas' => undef,
	'test_templatedatas' => undef,
	'test_requests' => undef,

	'test_database_type' => '',
	'dump_file' => undef,
	'author' => undef,
);


# do initializing package
&_test_prepare;

# overriding method for MT::*
# making MT to subclass of this package
# initializing MT::App::Test::Object
sub _test_prepare {
	require MT;

	# allowing multiple MT::App instance to exist
	my $add_plugin = \&MT::add_plugin;
	my $test_add_plugin = sub {
		eval { $add_plugin->(@_) };
		if ($@ && $@ !~ m/^You cannot register multiple plugin objects/) {
			die($@);
		}
	};
	

	# creating database if need
	my $init_config = \&MT::init_config;
	my $test_init_config = sub {
		my ($app, $param) = @_;
		my $cfg = $app->config;

		foreach my $k (keys %{ $param->{test_initialize_param} }) {
			$app->{$k} = $param->{test_initialize_param}{$k};
		}

		my $status = $init_config->(@_);
	
		if ($cfg->TestCreateDatabase) {
			my $object_driver = $cfg->ObjectDriver;

			if ($object_driver =~ m/DBI::(sqlite)/i) {
				$app->_test_prepare_database_sqlite();
			}
			elsif ($object_driver =~ m/DBI::(mysql)/i) {
				$app->_test_prepare_database_mysql();
			}
			elsif ($object_driver =~ m/DBI::(postgres)/i) {
				$app->_test_prepare_database_postgres();
			}

			$app->{test}{cache}{'test_database_type'} = $1;
		}
	
		# fixing directory settings
		my $config_dir = $app->{config_dir};
		my @mt_paths = $cfg->paths;
		my $mt_dir = dirname(dirname(File::Spec->canonpath($INC{'MT.pm'})));

		for my $meth (@mt_paths) {
			my $path = $cfg->get( $meth, undef );
			my $type = $cfg->type($meth);
	
			if ( $type eq 'ARRAY' ) {
				my @paths = $cfg->get($meth);
				local $_;
				foreach (@paths) {
					$_ =~ s/^$config_dir/$mt_dir/;
				}
				$cfg->$meth( \@paths );
			}
			else {
				$path =~ s/^$config_dir/$mt_dir/;
				$cfg->$meth($path);
			}
		}
	
		return $status;
	};

	require MT::App;
	# passing login process
	my $login = \&MT::App::login;
	my $test_login = sub {
		my ($app) = @_;
		if ($app->{test}{cache}{author}) {
			$app->user($app->{test}{cache}{author});
			return ($app->{test}{cache}{author}, 0);
		}
		else {
			return $login->(@_);
		}
	};

	{
		local $SIG{__WARN__} = sub {  };
		*MT::add_plugin = $test_add_plugin;
		*MT::init_config = $test_init_config;
		*MT::App::login = $test_login;
	}

	push(@MT::ISA, __PACKAGE__);
}

# Is database initializing needed ?
sub _test_is_initialize_database {
	my $self = shift;
	my ($param, $cache) = ($self->{test}{param}, $self->{test}{cache});
	if (
		(! -e $cache->{dump_file})
		|| $param->{'force_initialize_database'}
	) {
		return 1;
	}

	my $mtime = (stat($cache->{dump_file}))[9];

	$self->_test_is_fixtures_update($mtime)
		|| $self->_test_is_plugindatas_update($mtime);
}

# initializing process for sqlite
sub _test_prepare_database_sqlite {
	my $app = shift;
	my $cfg = $app->config;
	my ($param, $cache) = ($app->{test}{param}, $app->{test}{cache});

	my $db_file = $cfg->Database ||
		$app->_test_cat_base_dir('sqlite_database');
	$cfg->Database($db_file);

	if (! $cache->{dump_file}) {
		File::Path::mkpath(dirname($db_file));

		$cache->{dump_file} = my $dump_file =
			$app->_test_cat_base_dir('sqlite_dump_file');

		if ($app->_test_is_initialize_database) {
			if (! $app->isa('MT::App::Upgrader')) {
				$app->_test_initialize_database;

				File::Path::mkpath(dirname($dump_file));
				File::Copy::copy($db_file, $dump_file);
			}
		}
		else {
			File::Copy::copy($dump_file, $db_file);
		}
	}
}

# initializing process for mysql
sub _test_prepare_database_mysql {
	my $app = shift;
	my $cfg = $app->config;
	my ($param, $cache) = ($app->{test}{param}, $app->{test}{cache});

	if (! $cache->{dump_file}) {
		$cache->{dump_file} = my $dump_file =
			$app->_test_cat_base_dir('mysql_dump_file');

		if ($app->_test_is_initialize_database) {
			if (! $app->isa('MT::App::Upgrader')) {
				$app->_test_initialize_database;

				File::Path::mkpath(dirname($dump_file));
				system(
					'"' . $param->{mysqldump} . '" '
					. ' -u ' . $cfg->DBUser
					. ' -p' . $cfg->DBPassword
					. ' -h ' . $cfg->DBHost
					. ($cfg->DBPort ? (' -P ' . $cfg->DBPort) : '')
					. ' ' . $cfg->Database
					. ' > ' . $dump_file
				);
			}
		}
		else {
			system(
				'"' . $param->{mysql} . '" '
				. ' -u ' . $cfg->DBUser
				. ' -p' . $cfg->DBPassword
				. ' -h ' . $cfg->DBHost
				. ($cfg->DBPort ? (' -P ' . $cfg->DBPort) : '')
				. ' ' . $cfg->Database
				. ' < ' . $dump_file
			);
		}
	}
}

# initializing process for postgres
sub _test_prepare_database_postgres {
	my $app = shift;
	my $cfg = $app->config;
	my ($param, $cache) = ($app->{test}{param}, $app->{test}{cache});

	if (! $cache->{dump_file}) {
		$cache->{dump_file} = my $dump_file =
			$app->_test_cat_base_dir('postgres_dump_file');

		$ENV{PGPASSWORD} = $cfg->DBPassword;
		if ($app->_test_is_initialize_database) {
			if (! $app->isa('MT::App::Upgrader')) {
				$app->_test_initialize_database;

				File::Path::mkpath(dirname($dump_file));

				system(
					'"' . $param->{pg_dump} . '" -c '
					. ' -U ' . $cfg->DBUser
					. ' -h ' . $cfg->DBHost
					. ($cfg->DBPort ? (' -P ' . $cfg->DBPort) : '')
					. ' ' . $cfg->Database
					. ' > ' . $dump_file
				);
			}
		}
		else {
			system(
				'"' . $param->{psql} . '" -E -q '
				. ' -U ' . $cfg->DBUser
				. ' -h ' . $cfg->DBHost
				. ($cfg->DBPort ? (' -P ' . $cfg->DBPort) : '')
				. ' ' . $cfg->Database
				. ' < ' . $dump_file
			);
		}
	}
}

# merging yaml data that was placed in the specified directory
sub _test_merge_yaml_data {
	my $self = shift;
	my ($key) = @_;
	my ($param, $cache) = ($self->{test}{param}, $self->{test}{cache});
	if ($cache->{$key}) {
		return $cache->{$key};
	}


	my $dirs = $param->{$key};
	my %hash = ();

	foreach my $d (@$dirs) {
		foreach my $f (glob(File::Spec->catfile($d, '*.{yaml,yml}'))) {
			my $model = basename($f, '.yaml', '.yml');
			my $yaml = YAML::Tiny->read($f);
			my $datas = $yaml->[0];

			if (! $hash{$model}) {
				$hash{$model} = $datas;
			}
			else {
				$hash{$model}{$_} = $datas->{$_} foreach keys(%{ $datas });
			}

			if ($model eq 'blog') {
				foreach my $k (keys(%{ $hash{$model} })) {
					foreach my $kk (keys(%{ $hash{$model}{$k} })) {
						if (
							($kk eq 'path' || $kk eq 'site_path')
							&& ($kk !~ m#^/#)
						) {
							$hash{$model}{$k}{$kk} = File::Spec->catdir(
								$self->test_base_dir, $hash{$model}{$k}{$kk},
							);
						}
					}
				}
			}
		}
	}

	$cache->{$key} = \%hash;
}


# fetching data of these.
# "fixtures", "volatiles", "plugindatas", "templatedatas", "requests"
sub _test_fixtures {
	my $self = shift;
	$self->_test_merge_yaml_data('fixtures');
}

sub _test_volatiles {
	my $self = shift;
	$self->_test_merge_yaml_data('volatiles');
}

sub _test_plugindatas {
	my $self = shift;
	$self->_test_merge_yaml_data('plugindatas');
}

sub _test_templatedatas {
	my $self = shift;
	$self->_test_merge_yaml_data('templatedatas');
}

sub _test_requests {
	my $self = shift;
	$self->_test_merge_yaml_data('requests');
}


# checking modified time.
sub _test_is_update {
	my $self = shift;
	my ($key, $mtime) = @_;
	my ($param, $cache) = ($self->{test}{param}, $self->{test}{cache});

	foreach my $d (@{ $param->{$key} }) {
		foreach my $f (glob(File::Spec->catfile($d, '*.{yaml,yml}'))) {
			if ( (stat($f))[9] > $mtime ) {
				return 1;
			}
		}
	}

	return 0;
}

# checking modified time of plugindatas.
sub _test_is_plugindatas_update {
	my $self = shift;
	$self->_test_is_update('plugindatas', @_);
}

# checking modified time of plugindatas.
sub _test_is_fixtures_update {
	my $self = shift;
	$self->_test_is_update('fixtures', @_);
}


# initializing parameters
sub _test_initialize {
	my $self = shift;
	if ($self !~ m/^MT::App/) {
		croak('_test_initialize(): Must be called as class or instance method.');
	}

	my $opts = (@_ && ref $_[0]) ? $_[0] : { @_ };
	my ($param, $cache);
	if (ref $self) {
		($param, $cache) = ($self->{test}{param}, $self->{test}{cache});
	}
	else {
		$self = {
			test => {
				param => { %default_param },
				cache => { %initial_cache_status },
			},
		};
		($param, $cache) = ($self->{test}{param}, $self->{test}{cache});
	}

	my $create_base_dir = not $opts->{base_dir};

	foreach my $k (keys(%$opts)) {
		my $v = $opts->{$k};

		if ($k eq 'data_dirs') {
			foreach my $dd (ref $v ? @{ $v } : $v) {
				foreach my $d (glob(File::Spec->catdir($dd, '*'))) {
					my $k = basename($d);
					if (ref $param->{$k}) {
						push(@{ $param->{$k} }, $d);
					}
				}
			}
		}
		elsif (ref $param->{$k}) {
			push(@{ $param->{$k} }, (ref $v ? @$v : $v) );
		}
		else {
			$param->{$k} = $v;
		}
	}

	if ($create_base_dir) {
		my $p = { %$param };
		delete($p->{$_}) foreach (qw(
			base_dir force_initialize_database print_upgrade_progress
		));

		require Digest::MD5;
		require Data::Dumper;

		local $Data::Dumper::Sortkeys = 1;

		my $md5 = Digest::MD5::md5_hex(
			Data::Dumper::Dumper($p)
			. File::Spec->canonpath($INC{'MT.pm'})
		);
		
		$param->{base_dir} = File::Spec->catfile(
			File::Spec->tmpdir, $md5
		);
	}

	$self;
}

# fetching steps to install
sub _test_install_steps {
	my ($app) = @_;

    my $new_objs = {};

	my $fixtures = $app->_test_fixtures;

	my %models = qw(
		author user
		blog blog
	);
	foreach my $model (keys(%models)) {
		if (my $i = $fixtures->{$model}{initial}) {
			$new_objs->{$model} = {};
			my $prefix = $models{$model};
			foreach my $k (keys(%$i)) {
				$new_objs->{$model}{$prefix . '_' . $k} = $i->{$k};
			}
		}
	}

	local $app->{upgrading} = 1;
	require MT::Upgrade;

	MT::Upgrade->do_upgrade(
		Install => 1,
		DryRun  => 0,
		App     => $app,
		User => $new_objs->{author},
		Blog => $new_objs->{blog},
	);
	my $steps = $app->response->{steps};
	my $fn    = \%MT::Upgrade::functions;
	if ( $steps && @$steps ) {
		@$steps = sort {
			$fn->{ $a->[0] }->{priority} <=> $fn->{ $b->[0] }->{priority}
		} @$steps;
	}

	$steps;
}

# initializing database
sub _test_initialize_database {
	my $self = shift;
	my ($param, $cache) = ($self->{test}{param}, $self->{test}{cache});
	if (! ref $self) {
		croak('_test_initialize_database(): Must be called as instance method.');
	}

	if (-f $self->config->Database) {
		unlink($self->config->Database);
	}

	$self->_test_initialize(@_);

	require MT::App::Upgrader;
	my $app = MT::App::Upgrader->new(
		Config => $self->{cfg_file},
		test_initialize_param => {
			'test' => $self->{test},
		},
	);
	if (! $param->{print_upgrade_progress}) {
		local $SIG{__WARN__} = sub {  };
		my $print = \&MT::App::Upgrader::print;
		*MT::App::Upgrader::print = sub {
			1;
		};
	}

	{
		my $driver = MT::Object->driver;
		my $dbh = $driver->rw_handle;
		foreach my $model (keys(%{ $app->registry('object_types') })) {
			my $model_class = $app->model($model);
			my $source = $model_class->datasource;
			my $table = 'mt_' . $source;
			$dbh->do("DROP TABLE $table;");

			if (my $meta_class = $app->model($model . ':meta')) {
				my $source = $meta_class->datasource;
				my $table = 'mt_' . $source;
				$dbh->do("DROP TABLE $table;");
			}
		}
	}

    my $steps = &_test_install_steps($app);

	my @steps = (@$steps);
	my $step;

	eval {
		require MT::Upgrade;

		MT::Upgrade->init;

		local (
			$MT::Upgrade::App,
			$MT::Upgrade::Installing,
			$MT::Upgrade::MAX_TIME,
		);
		$MT::Upgrade::App        = $app;
		$MT::Upgrade::Installing = 1;
		$MT::Upgrade::MAX_TIME   = 9999;

		my $fn    = \%MT::Upgrade::functions;

		while ( $step = shift @steps ) {
			my $result    = MT::Upgrade->run_step($step);
			my $new_steps = $app->response->{steps};
			if (@$new_steps) {
				push @steps, @$new_steps;
				@steps = sort {
					$fn->{ $a->[0] }->{priority} <=> $fn->{ $b->[0] }
						->{priority}
				} @steps;
				$app->response->{steps} = [];
			}

			#last unless $result;
		}
	};

	die $@ if $@;

	my $fixtures = $self->_test_fixtures;
	foreach my $mk (keys(%$fixtures)) {
		my $model = $self->model($mk);
		if (! $model) {
			croak('MT->model(\'' . $mk . '\') is undefined.');
		}
		foreach my $ok (keys(%{ $fixtures->{$mk} })) {
			if ($ok eq 'initial') {
				next;
			}

			my $hash = $fixtures->{$mk}{$ok};
			my $obj = $model->new;
			foreach my $k (keys(%$hash)) {
				$obj->$k($hash->{$k});
			}
			$obj->save or croak $@;
		}
	}
	
	my $plugindatas = $self->_test_plugindatas;
	foreach my $pk (keys(%$plugindatas)) {
		my $plugin = $self->component($pk);
		if (! $plugin) {
			croak('MT->componet(\'' . $pk . '\') is undefined.');
		}
		foreach my $scope (keys(%{ $plugindatas->{$pk} })) {
			$plugin->set_config_value($plugindatas->{$pk}{$scope}, $scope);
		}
	}

	1;
}


# Can I PHP test
sub test_is_php_testable {
	my $self = shift;
	if ($self->{test}{param}{php}) {
		my ($ver) = `$self->{test}{param}{php} --version`;
		$ver =~ m/^php\s*5\./i;
	}
	else {
		0;
	}
}


# base directory for test
sub test_base_dir {
	my $self = shift;
	$self->{test}{param}{base_dir};
}

sub _test_cat_base_dir {
	my $self = shift;
	File::Spec->catfile(
		$self->{test}{param}{base_dir},
		$self->{test}{param}{$_[0]},
	);
}

# fetching instance
sub test_instance_of {
	my $package = __PACKAGE__;
	if (@_ && $_[0] =~ m/^$package/) {
		shift;
	}

	my %init_param = ();
	my $opts = (@_ && ref $_[0]) ? $_[0] : { @_ };

	my $class = $opts->{isa} || 'MT::App::CMS';
	delete($opts->{isa});

	$init_param{Config} = $default_config;
	if (exists($opts->{'mt-config.cgi'})) {
		$init_param{Config} = $opts->{'mt-config.cgi'};
		delete($opts->{'mt-config.cgi'});
	}
	$ENV{MT_CONFIG} = $init_param{Config};

	require File::Spec->catfile(split(/::/, $class)) . '.pm';

	$init_param{test_initialize_param} = $class->_test_initialize($opts);

	my $self = $class->new(%init_param);

	$self;
}


# fetching App's Log
sub test_log {
	my @logs = MT::Log->load;
	if (wantarray) {
		@logs;
	}
	else {
		join("\n", map({ $_->message } @logs));
	}
}


# geting model class.
sub test_model {
	my $app = shift;
	my $model = $app->model(@_);
}


sub _test_load_object_in_hash {
	my $self = shift;
	my $hash = shift;
	if (! $hash) {
		return;
	}

	foreach my $k (keys(%$hash)) {
		if (! $hash->{$k}) {
			if ($hash->{$k . '_id'}) {
				my $model = $self->model($k);
				if (ref $hash->{$k . '_id'}) {
					$hash->{$k} = [
						$hash->{$k} = $model->load({
							id => $hash->{$k . '_id'},
						})
					];
				}
				else {
					$hash->{$k} = $model->load($hash->{$k . '_id'});
				}
			}
		}
		elsif (ref $hash->{$k} eq 'HASH') {
			$self->_test_load_object_in_hash($hash->{$k});
		}
	}
}


sub _test_copy_deep {
	my $data = shift;
	if (ref $data eq 'HASH') {
		my $copy = {};
		$copy->{$_} = &_test_copy_deep($data->{$_}) foreach (keys(%$data));
		$copy;
	}
	elsif (ref $data eq 'ARRAY') {
		[
			map(&_test_copy_deep($_), @$data)
		];
	}
	elsif (ref $data eq 'SCALAR') {
		my $data = $$data;
		\$data;
	}
	else {
		$data;
	}
}

# fetching defined context's stash data
sub test_template_load_context {
	my $self = shift;
	my ($id) = @_;

	$self->{context} ||= MT::Template::Context->new;
	$self->{loaded_context} ||= [];

	my $datas = $self->_test_templatedatas;
	if (! $datas->{context}{$id}) {
		croak('' . $id . ' has not been registered yet.');
	}
	my $hash = $datas->{context}{$id};

	push(
		@{ $self->{loaded_context} },
		&_test_copy_deep($hash)
	);

	$self->_test_load_object_in_hash($hash);

	foreach my $k (keys(%$hash)) {
		$self->{context}{$k} = $hash->{$k};
	}
	
	1;
}


# building template
sub test_template_build {
	my $self = shift;
	my ($file, $param) = @_;

	my $tmpl = undef;
    my $tmpl_file = '';
    if (UNIVERSAL::isa($file, 'MT::Template')) {
        $tmpl = $file;
    }
	else {
		my $type = {'SCALAR' => 'scalarref', 'ARRAY' => 'arrayref'}->{ref $file}
			|| 'filename';

		require MT::Template;

		$tmpl = MT::Template->new(
			type => $type,
			source => $file,
			path => [],
		);
    }

	$tmpl->context($self->{context}) if $self->{context};
	$tmpl->param($param) if $param;

    my $out = $tmpl->output;
    return $self->error($tmpl->errstr) unless defined $out;
    return $self->translate_templatized($self->process_mt_template($out));
}


sub _test_init_php {
	my $self = shift;
	my ($param, $cache) = ($self->{test}{param}, $self->{test}{cache});
	$self->{php} ||= {};

	if ($self->{php}->{in}) {
		return $self->{php};
	}

	my $php = $self->{php};
	(my $file = __FILE__) =~ s/\.pm//;
	$file = File::Spec->catfile($file, 'test.php');
	my $script = do{ open(my $fh, '<', $file); local $/; <$fh> };

	my $mt_dir = dirname(dirname(File::Spec->canonpath($INC{'MT.pm'})));
	my $plugin_path = File::Spec->catfile($mt_dir, 'plugins');

	my $tempfile_unlink = 1;
	require File::Temp;
	my ($fh, $mt_config_cgi) = File::Temp::tempfile(UNLINK => $tempfile_unlink);
	print($fh do{
		open(my $fh, '<', $self->{cfg_file}); local $/; <$fh>;
	});
	if ($cache->{'test_database_type'} eq 'sqlite') {
		print($fh 'Database ' . $self->_test_cat_base_dir('sqlite_database') . "\n");
	}
	print($fh 'PluginPath ' . $plugin_path . "\n");
	close($fh);

	my ($fh2, $newtest_php) = File::Temp::tempfile(UNLINK => $tempfile_unlink);
	print($fh2 <<__EOP__);
<?php
set_include_path(get_include_path() . ':' . '$mt_dir');
?>
$script
<?php
test_run(array(
	'mt-config.cgi' => '$mt_config_cgi',
));
?>
__EOP__
	close($fh2);

    $php->{pid} = open2(my $in, my $out, $param->{php}, '-f', $newtest_php);
	my $orig_handle = select $in; $| = 1;
	select $out; $| = 1;
	select $orig_handle;
	($php->{in}, $php->{out}) = ($in, $out);
	
	return $php;
}

# building template by php
sub test_php_template_build {
	my $self = shift;
	my ($file, $param) = @_;
	my $str = '';

	if (ref $file eq 'SCALAR') {
		$str = $$file;
	}
	elsif (ref $file eq 'ARRAY') {
		$str = join('', @$file);
	}
	else {
		$str = do{ open(my $fh, '<', $file); local $/; <$fh> };
	}

	my $mt = $self->_test_init_php;
	my ($in, $out) = ($mt->{in}, $mt->{out});
	
	if ($self->{loaded_context}) {
		my $json = objToJson($self->{loaded_context});
		print($out "test_template_load_context('$json');\n");
	}

	if ($param) {
		my $json = objToJson($param);
		print($out "test_template_param('$json');\n");
	}

	$str =~ s/\\/\\\\/g;
	$str =~ s/"/\\"/g;
	$str =~ s/\r\n|\r|\n/\\n/g;

	print($out "test_template_build(\$mt, \"$str\");\n");

	my $selector = IO::Select->new($in);
	my $ret = '';
	while($selector->can_read(1)) {
		$ret .= <$in>;
		if ($ret !~ m/^\s*{/) {
			print $ret;
			$ret = '';
		}
	}

	my $obj = jsonToObj($ret);
	if ($obj->{error}) {
		croak($obj->{result});
	}
	else {
		return $obj->{result};
	}
}


# building a page
# This method is alias to "MT->build_page()".
sub test_build_page {
	my $self = shift;
	$self->build_page(@_);
}


# setting App's parameters
sub test_request_load_param {
	my $app = shift;
	my ($p) = $app->_test_request_load_param(@_);
	$app->param($_, $p->{$_}) foreach (keys(%$p));

	$p;
}

# setting App's parameters inner mehtod
sub _test_request_load_param {
	my $app = shift;

	my $p = {};
	my $author = undef;
	if (scalar(@_) == 1) {
		my $datas = $app->_test_requests;
		my ($id) = @_;
		if (! $datas->{param}{$id}) {
			croak('"' . $id . '" has not been registered yet.');
		}
		$p = $datas->{param}{$id};

		if ($datas->{author}{$id}) {
			my $author_class = $app->test_model('author');
			$author = $author_class->load(
				$datas->{author}{$id}{id}
			);
		}
	}
	else {
		$p = { @_ };
	}

	($p, $author);
}


# requesting
sub test_request {
	my $self = shift;
	my ($param, $cache) = ($self->{test}{param}, $self->{test}{cache});
	my ($p, $author) = $self->_test_request_load_param(@_);
	$cache->{author} = $author;

	my $cgi = new CGI($p);

	MT->set_instance($self);
	$self->init_request( CGIObject => $cgi );

	my $html = '';
	my $print = \&MT::App::print;
	{
		local $SIG{__WARN__} = sub {  };
		*MT::App::print = sub {
			my $app = shift;
			$html = join('', @_);
		};
	}
	$self->run;

	$html;
}


1;

__END__

=head1 NAME

MT::App::Test - Movable Type's utility for test


=head1 VERSION

This document describes MT::App::Test version 0.0.1


=head1 SYNOPSIS

  use MT::App::Test;

  use File::Basename qw( dirname );
  use File::Spec;

  # createing test app object
  $app = MT::App::Test->test_instance_of(
    data_dirs => dirname(File::Spec->rel2abs(__FILE__)),
  );
  $app->isa('MT::App::CMS'); # => true


  # model class for testing
  my $entry_class = $app->test_model('entry');

  # fetching entry that has not stored.
  my $entry_to_save = $entry_class->test_load_volatiles('to_save');
  $entry_to_save->save();


  # loading a "context" for test
  $app->test_template_load_context('ctx_1'),

  # building tag
  $entry_title = $app->test_template_build(\'<mt:EntryTitle>');

  # building tag by php
  $entry_title = $app->test_php_template_build(\'<mt:EntryTitle>');


  # building page
  $html = $app->test_build_page('dashboard.tmpl'),


  # requesting
  $html = $app->test_request('req_1'));
  
  
=head1 DESCRIPTION

This package will help you to write "test".


=head1 INTERFACE 

=over

=item test_instance_of

getting App instance which extended for testing.

  my $data_dir = dirname(File::Spec->rel2abs(__FILE__));
  $app = MT::App::Test->test_instance_of(
    isa => 'MT::App::CMS', # default
    # isa => 'MT::App::Search', # search

    # directory cotains "fixtures", "volatiles", ...
    data_dirs => $data_dir,

    # specifying each directory.
    # fixtures => File::Spec->catdir($data_dir, 'fixtures'),
    # volatiles => File::Spec->catdir($data_dir, 'volatiles'),
    # plugindatas => File::Spec->catdir($data_dir, 'plugindatas'),
    # templatedatas => File::Spec->catdir($data_dir, 'templatedatas'),
    # requests => File::Spec->catdir($data_dir, 'requests'),

    # specifying "mt-config.cgi"
    'mt-config.cgi' => File::Spec->catdir($data_dir, 'mt-config.cgi'),

    print_upgrade_progress => 0, # default
    # print_upgrade_progress => 1, # print log message when upgrading

    # parameters for sqlite
    # sqlite_database => 'mt-sqlite.db', # default
    # sqlite_dump_file => 'mt-sqlite-dump.db', # default
    
    # parameters for mysql
    # mysql_dump_file => 'mt-mysql-dump.db', # default
    # mysql => 'mysql', # default
    # mysqldump => 'mysqldump', # default
    
    # parameters for postgres
    # postgres_dump_file => 'mt-postgres-dump.db', # default
    # psql => 'psql', # default
    # pg_dump => 'pg_dump', # default
    
    # parameters for php
    # php => 'php', # default

  });

=item test_model

getting model class which extended for testing.

  my $entry_class = $app->test_model('entry');

=item test_base_dir

base directory of test.
base directory contains following data.
  - database
  - blog that has published

  my $base_dir = $app->test_base_dir

=item test_log

getting App's log message.

  print $app->test_log

=item test_template_load_context

loading template's parameters.

when $data/templatedatas/context.yaml contains

  ctx_1:
    archive_type: Individual

    __stash:
      blog_id: 2
      blog: ~
      entry_id: 2
      entry: ~

      vars:
        var1: one
        var2: two

loading to template's context

  $app->test_template_load_context('ctx_1');

then

  $blog = $app->context->stash('blog');
  $blog->id # => 2
  $entry = $app->context->stash('entry');
  $entry->id # => 2

=item test_template_build

processing a tag.

  # filename
  $contents = $app->test_template_build('template_file');
  # string
  $contents = $app->test_template_build(\'<mt:Unless>abc</mt:Unless>');

=item test_is_php_testable

Can I PHP test (PHP >= 5 is required)

  $testable = $app->test_is_php_testable;

=item test_php_template_build

processing a tag through php.

  # filename
  $contents = $app->test_php_template_build('template_file');
  # string
  $contents = $app->test_php_template_build(\'<mt:Unless>abc</mt:Unless>');


=item test_build_page

building a page's html.

  $html = $app->test_build_page('dashboard.tmpl');
 

=item test_request_load_param

loading CGI's request parameters.

when $data/requests/param.yaml contains

  req_1:
    __mode: cfg_plugins
    blog_id: 1

and $data/requests/author.yaml contains

  req_1:
    id: 1

and loading

  $app->test_request_load_param('req_1');

then

  $app->param('__mode') # => 'cfg_plugins'
  $user = $app->user;
  $user->id # => 1

=item test_request

emulating request.

  $app->test_request_load_param('req_1');
  $html = $app->test_request;

=back

=head1 BUGS AND LIMITATIONS

Please report any bugs or feature requests to
C<bug-mt-app-test@rt.cpan.org>, or through the web interface at
L<http://rt.cpan.org>.


=head1 AUTHOR

Taku Amano  C<< <taku@toi-planning.net> >>


=head1 LICENCE AND COPYRIGHT

Copyright (c) 2008, Taku Amano C<< <taku@toi-planning.net> >>. All rights reserved.

This module is free software; you can redistribute it and/or
modify it under the same terms as Perl itself. See L<perlartistic>.


=head1 DISCLAIMER OF WARRANTY

BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER
EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE
ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH
YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL
NECESSARY SERVICING, REPAIR, OR CORRECTION.

IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE
LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL,
OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE
THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
